﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using LaunchMeNot.Core;
using LaunchMeNot.Core.Loggers;
using Lime49;
using Lime49.Exceptions;

namespace LaunchMeNot.DAL
{
    /// <summary>
    /// Stores LaunchMeNot entries in an XML File
    /// </summary>
    public class XMLStore : IEntryStore {
        private ILogProvider _logger;

        /// <summary>
        /// Gets or sets path of the file used to store entries.
        /// </summary>
        /// <value>The path of the file used to store entries.</value>
        public string FilePath { get; set; }

        /// <summary>
        /// Initializes a new <see cref="XMLStore"/>.
        /// </summary>
        /// <param name="xmlPath">The file in which to store entries.</param>
        public XMLStore(string xmlPath) {
            this.FilePath = xmlPath;
            _logger = new ConsoleLogger();
        }

        /// <summary>
        /// Adds a new startup entry.
        /// </summary>
        /// <param name="entry">The entry.</param>
        public void AddStartupEntry(IStartupEntry entry)
        {
            LaunchMeNotEntry realEntry = entry as LaunchMeNotEntry;
            if(realEntry == null)
                throw new ArgumentException("Only LaunchMeNotEntries can be managed by LaunchMeNot");
            List<LaunchMeNotEntry> entries = GetStartupEntries().Cast<LaunchMeNotEntry>().ToList();
            if(entries.FirstOrDefault(e=>e.ID == realEntry.ID) != null)
                throw new LaunchMeNotException(650, "An entry with that ID already exists");
            entries.Add(realEntry);
            SaveDatabase(entries);
        }

        /// <summary>
        /// Adds multiple new startup entries.
        /// </summary>
        /// <param name="entries">The entries.</param>
        public void AddStartupEntries(IEnumerable<IStartupEntry> entries) {
            IEnumerable<LaunchMeNotEntry> realEntries;
            try {
                realEntries = entries.Cast<LaunchMeNotEntry>();
            } catch {
                throw new ArgumentException("Only LaunchMeNotEntries can be managed by LaunchMeNot");
            }

            List<LaunchMeNotEntry> existingEntries = GetStartupEntries().Cast<LaunchMeNotEntry>().ToList();
            var existingIds = existingEntries.Select(ent=>ent.ID);
            existingEntries.AddRange(realEntries.Where(e => !existingIds.Contains(e.ID)));
            SaveDatabase(existingEntries);
        }

        /// <summary>
        /// Edits a startup entry.
        /// </summary>
        /// <param name="entry">The entry.</param>
        public void EditStartupEntry(IStartupEntry entry)
        {
            LaunchMeNotEntry realEntry = entry as LaunchMeNotEntry;
            if (realEntry == null)
                throw new ArgumentException("Only LaunchMeNotEntries can be managed by LaunchMeNot");
            List<LaunchMeNotEntry> entries = GetStartupEntries().Cast<LaunchMeNotEntry>().ToList();
            entries.RemoveAll(e => e.ID == realEntry.ID);
            entries.Add(realEntry);
            SaveDatabase(entries);
        }

        /// <summary>
        /// Edits multiple startup entries.
        /// </summary>
        /// <param name="entries">The entries.</param>
        public void EditStartupEntries(IEnumerable<IStartupEntry> entries)
        {
            if (!(entries is IEnumerable<LaunchMeNotEntry>))
                throw new ArgumentException("Only LaunchMeNotEntries can be managed by LaunchMeNot");
            EditStartupEntries(entries.Cast<IStartupEntry>());
        }

        /// <summary>
        /// Edits multiple startup entries.
        /// </summary>
        /// <param name="entries">The entries.</param>
        public void EditStartupEntries(IEnumerable<LaunchMeNotEntry> entries)
        {
            var idsToDelete = entries.Select(e => e.ID);
            List<LaunchMeNotEntry> existingEntries = GetStartupEntries().Cast<LaunchMeNotEntry>().ToList();
            existingEntries.RemoveAll(e => idsToDelete.Contains(e.ID));
            existingEntries.AddRange(entries);
            SaveDatabase(existingEntries);
        }

        /// <summary>
        /// Deletes a startup entry.
        /// </summary>
        /// <param name="entry">The entry.</param>
        public void DeleteStartupEntry(IStartupEntry entry)
        {
            DeleteStartupEntries(new[] { entry });
        }

        /// <summary>
        /// Deletes multiple startup entries.
        /// </summary>
        /// <param name="entries">The entries.</param>
        public void DeleteStartupEntries(IEnumerable<IStartupEntry> entries)
        {
            if(entries.Any(e=>!(e is LaunchMeNotEntry)))
                throw new ArgumentException("Only LaunchMeNotEntries can be managed by LaunchMeNot");
            DeleteStartupEntries(entries.Cast<LaunchMeNotEntry>());
        }

        /// <summary>
        /// Deletes multiple startup entries.
        /// </summary>
        /// <param name="entries">The entries.</param>
        public void DeleteStartupEntries(IEnumerable<LaunchMeNotEntry> entries)
        {
            List<LaunchMeNotEntry> existingEntries = GetStartupEntries().Cast<LaunchMeNotEntry>()
                                                                        .OrderBy(e => e.LaunchOrder)
                                                                        .ToList();
            var idsToDelete = entries.Select(e => e.ID);
            existingEntries.RemoveAll(e => idsToDelete.Contains(e.ID));
            for (int i = 0; i < existingEntries.Count; i++) {
                existingEntries[i].LaunchOrder = i;
            }
            SaveDatabase(existingEntries);
        }

        /// <summary>
        /// Gets a list of all startup entries managed by LaunchMeNot.
        /// </summary>
        /// <returns>All startup entries managed by LaunchMeNot.</returns>
        public IList<IStartupEntry> GetStartupEntries() {
            XDocument doc = ReadDatabaseDocument();
            List<IStartupEntry> entries = new List<IStartupEntry>();
            IEnumerable<XElement> entryNodes = doc.Root.Element("entries").Elements();
            foreach (XElement xEntry in entryNodes) {
                try {
                    entries.Add(new LaunchMeNotEntry(xEntry));
                } catch (Exception ex) {
                    _logger.Log(string.Format("Invalid entry: {0}", ex.Message), true);
                }
            }
            return entries;
        }

        /// <summary>
        /// Reads an XDocument from the database file.
        /// </summary>
        /// <returns>The current database contents.</returns>
        private XDocument ReadDatabaseDocument() {
            XDocument doc;
            try {
                doc = File.Exists(FilePath) ? XDocument.Load(FilePath) : SaveDatabase(new LaunchMeNotEntry[0]);
                return doc;
            } catch(Exception innerEx) {
                throw new Exception(I18nUtils.GetString("Errors", "ErrorLoadingDatabase") + ": " + innerEx.Message, innerEx);
            }
        }

        /// <summary>
        /// Saves the file, then returns the XDocument which was saved.
        /// </summary>
        /// <returns>An XDocument containing the specified <c>entries</c>.</returns>
        private XDocument SaveDatabase(IEnumerable<LaunchMeNotEntry> entries) {
            XDocument doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"));
            XElement root = new XElement("launchmenot");
            doc.Add(root);
            XElement meta = new XElement("meta",
                new XElement("generator", "LaunchMeNot " + Lime49.Utils.GetVersion().ToString(3)),
                new XElement("date", DateTime.Now.ToUniversalTime().ToString("s")));
            root.Add(meta);
            MultipleException errors = new MultipleException();

            int numItems = 0;
            XElement xEntries = new XElement("entries");
            root.Add(xEntries);
            foreach(var entry in entries) {
                try {
                    xEntries.Add(entry.ToXElement());
                    numItems++;
                } catch(Exception ex) {
                    errors.Exceptions.Add(ex);
                }
            }

            doc.Save(FilePath);
            return doc;
        }

        /// <summary>
        /// Gets the highest launch order currently in use.
        /// </summary>
        /// <returns>The highest launch order currently in use.</returns>
        public int GetMaxLaunchOrder()
        {
            int highestLaunchOrder = -1;
            IEnumerable<LaunchMeNotEntry> existingEntries = GetStartupEntries().Cast<LaunchMeNotEntry>();
            if (existingEntries.Any()) {
                highestLaunchOrder = existingEntries.Max(e => e.LaunchOrder);
            }
            return highestLaunchOrder;
        }
    }
}
